# This file is part of WSAP web sockets server module.
# Copyright (c) 2010 Brandon Orther. All rights reserved.

from core import *
from json_envelope import *

import json, select, socket


class server:
    """
    WSAP(WebSockets App) Server is a python module that makes developing web socket application
    servers a breeze.
    """

    callable_tasks = [] # This holds the methods that can be called from websocket requests
    EOL = [b"\r\n\r\n", b"\xFF"] # This handles http header and websocket frame EOL

    def __init__(self, host, port, callable_tasks, debug=False):
        """
        Validate server settings and then start server.

        Params:
            (String)    host:   Host to bind to.
            (Integer)   port:   Port to listen on.
            (Boolean)   debug:  When set to true debug info is printed to the console. Defaults to
                                False.
        """
        self.host           = host
        self.port           = port
        self.callable_tasks = callable_tasks
        self.debug          = debug

        self.validate_settings()
        self.server()

    def default_task(self, fileno, data_obj):
        """
        This method is called when a socket request calls a task that has not been added to the
        call list. The child class can overload this method to handle it otherwise an exception
        will be thrown and printed to console.
        """
        raise InvalidTask("Invalid task called from websocket client. No default task declared.")

    def socket_disconnect(self, fileno):
        """
        This method is called when a socket is disconnected. The child class can overload this
        method to process socket disconnects.

        Params:
            (Integer)   fileno
        """
        pass

    def handler(self, data, fileno):
        """
        Use parent method parse_request_header to catch detault Web Socket request header and
        return a valid web socket response header. If the incoming data is not a request header
        then handle parse the data and perform some type of action.

        NOTE: If you do not validate/authenticate the origin of your request in some way any valid
              request header will be able to open a socket.

        Params: 
            (String)    data: Incoming data from socket.
            (Int)       fileno: The file number for this socket connection 

        Return: (Mixed) If there is a valid reponse a string is returned, otherwise False is
                        returned.
        """
        response_header = server.parse_request_header(self, data)
        if response_header:
            return response_header
        try:
            # Remove WebSocket frame and decode JSON
            data_obj = json.loads(data[1:-1])
            if data_obj["task"] in self.callable_tasks:
                return getattr(self, data_obj["task"])(fileno, data_obj)
            else:
                self.default_task(fileno, data_obj)
        except Exception,e:
            print ("Exception",e)
        return False

    def parse_request_header(self, request_header):
        """
        Parse request header. If not request header return false otherwise return response header.

        Params: (String)    request_header
        """
        request_header = request_header.split("\r\n")
        if request_header[0][:4] == "GET " and request_header[0][-9:] == " HTTP/1.1":
            request_details = {"Path": request_header[0][4:-9]}
            for line in request_header[1:]:
                try:
                    field, value = line.split(": ",1)
                    if field in ["Host","Origin"] and value:
                        request_details[field] = value
                except:
                    pass
            return u"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +\
                   u"Upgrade: WebSocket\r\n" +\
                   u"Connection: Upgrade\r\n" +\
                   u"WebSocket-Origin: %s\r\n" % request_details['Origin'] +\
                   u"WebSocket-Location: ws://%s"  % request_details['Host'] +\
                   u"%s\r\n"  % request_details['Path'] +\
                   u"WebSocket-Protocol: sample\n".strip() + '\r\n\r\n'
        else:
            return False

    def push(self, data, ignore=[]):
        """
        TODO: Move this into the main server

        Push data to all current connections expluding the ones passed in a list (blocked_fileno)

        Params:
            (String)    data: Data to be sent to other active connections
            (List)      ignore: This is a list of fileno Int's that will be ignored when adding
                                data to the output buffers.  Essnetially any fileno in this list
                                will not get this update.
        """
        for fileno in self.responses:
            if fileno not in ignore:
                self.responses[fileno] = data
                self.epoll.modify(fileno, select.EPOLLOUT | select.EPOLLET)

    def server(self):
        """
        Asyncrynous WebSockets server.
        """
        # Start main server socket that clients connect to to get issued their own socket.  
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server_socket.bind((self.host, self.port))
        server_socket.listen(1)
        server_socket.setblocking(0)

        self.epoll = select.epoll()
        self.epoll.register(server_socket.fileno(), select.EPOLLIN | select.EPOLLET)

        try:
            connections = {}; requests = {}; self.responses = {}
            while True:
                events = self.epoll.poll(1)
                for fileno, event in events:

                    # For all incoming connections create a new socket for that client to
                    # communicate through during their duration of their stay.  Then register the
                    # connection with epoll and add it to the queues.
                    if fileno == server_socket.fileno():
                        try:
                            connection, address = server_socket.accept()
                            connection.setblocking(0)
                            self.epoll.register(connection.fileno(), select.EPOLLIN | select.EPOLLET)
                            connections[connection.fileno()] = connection
                            requests[connection.fileno()]  = b""
                            self.responses[connection.fileno()] = b""
                        except socket.error, e:
                            if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
                                continue
                            self.epoll.unregister(fileno)
                            connections[fileno].close()
                            del connections[fileno]
                            self.socket_disconnect(fileno)
                            continue

                    # Push the incoming data to the handler. After the data is sent we set the
                    # socket ready for ouput and edge triggered. Before we try to read in all the
                    # data we must check if the input buffer is empty. If the EPOLLIN event was
                    # trigger with the input buffer empty then the connection with the client has
                    # closed so we need to shutdown the socket. 
                    elif event & select.EPOLLIN:
                        try:
                            requests[fileno] = connections[fileno].recv(1024)
                            if len(requests[fileno]) == 0:
                                # Client disconnected so we shutdown socket
                                connections[fileno].shutdown(socket.SHUT_RDWR)
                                self.epoll.modify(fileno, select.EPOLLHUP)
                            else:
                                while True:
                                    requests[fileno] += connections[fileno].recv(1024)
                        except socket.error,e:
                            if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
                                continue
                            self.epoll.unregister(fileno)
                            connections[fileno].close()
                            del connections[fileno]
                            self.socket_disconnect(fileno)
                            continue
                        for item in self.EOL:
                            if item in requests[fileno]:
                                self.epoll.modify(fileno, select.EPOLLOUT | select.EPOLLET)
                                resp = self.handler(requests[fileno], fileno)
                                requests[fileno] = b""
                                if (resp):
                                    self.responses[fileno] = resp

                    # Read from output buffer building up the data to be sent to the client.  Once
                    # the buffer is empty send the data to client and set epoll status for this
                    # socket ready for input and edge triggered.
                    elif event & select.EPOLLOUT:
                        try:
                            while len(self.responses[fileno]) > 0:
                                byteswritten = connections[fileno].send(self.responses[fileno])
                                self.responses[fileno] = self.responses[fileno][byteswritten:]
                        except socket.error:
                            pass
                        if len(self.responses[fileno]) == 0:
                            self.epoll.modify(fileno, select.EPOLLIN | select.EPOLLET)

                    # Handle a epoll socket hangups. When the socket is finished we clean it up
                    # here. Unregister it from epoll, close the connection and then delete the
                    # connection all together.
                    elif event & select.EPOLLHUP:
                        self.epoll.unregister(fileno)
                        connections[fileno].close()
                        del connections[fileno]
                        self.socket_disconnect(fileno)

        # Close epoll and socket server when the server shuts down
        finally:
            self.epoll.unregister(server_socket.fileno())
            self.epoll.close()
            server_socket.close()

    def validate_settings(self):
        """
        Make sure all required settings are set and valid before trying to start the server.

        Note:   This method returns null and throws a WsapException on error.
        
        Return:
            (Null)  Nahdah, zilch, zippo, nothing...
        """

        # TODO: Add real validation. Steal Seans validation stuff muhahahahahaha!
        if not self.host:
            raise WsapException("Invalid server host.")
        if not self.port or type(self.port) is not int:
            raise WsapException("Invalid server port.")
        if not self.port or type(self.callable_tasks) is not list:
            raise WsapException("Invalid callable tasks list. Must be a list.")
        if not self.EOL or type(self.EOL) is not list:
            raise WsapException("At least 1 EOL delimeter required, none set.")
